#ifndef GLSLPROGRAM_H
#define GLSLPROGRAM_H

#include <iostream>
#include <cstdlib>
#include <string>
#include <fstream>
#include <streambuf>

#ifdef __APPLE__
#include <OpenGL/gl3.h>
#else
#include <glew.h>
#include <GL/gl.h>
#endif

#include <glm/glm.hpp>
#include <glm/ext.hpp>

class GLSLProgram
{
private:
    GLuint handle;
    bool isLinked;

private:
    inline GLuint GetUniformLocation(const char *name);

public:
    GLSLProgram();
    bool AddShaderFromFile(std::string filename, GLenum type);
    bool AddShaderFromString(const std::string &progContent, GLenum type);
    bool Validation();
    bool Link();
    bool IsLinked() {return isLinked;}
    void Use() {glUseProgram(handle);}
    GLuint GetHandle() {return handle;}
    inline void BindAttribLocation(GLuint location, const char *name);
    inline void BindFragDataLocation(GLuint location, const char *name);

    inline void SetUniform(const char *name, float x, float y, float z);
    inline void SetUniform(const char *name, float x, float y, float z, float w);
    inline void SetUniform(const char *name, const glm::vec2 &vec);
    inline void SetUniform(const char *name, const glm::vec3 &vec);
    inline void SetUniform(const char *name, const glm::vec4 &vec);
    inline void SetUniform(const char *name, const glm::mat3 &mat);
    inline void SetUniform(const char *name, const glm::mat4 &mat);
    inline void SetUniform(const char *name, int val);
    inline void SetUniform(const char *name, float val);
    inline void SetUniform(const char *name, bool val);

    void PrintActiveUniforms();
    void PrintActiveAttribs();
};

inline GLuint GLSLProgram::GetUniformLocation(const char *name)
{
    GLuint loc = glGetUniformLocation(handle, name);
#ifdef _DEBUG
    GLenum err = glGetError();
    if(err == GL_INVALID_VALUE)
    {
        std::cerr<<"Invalid uniform name!\n"<<std::endl;
    }
#endif
    return loc;
}

inline void GLSLProgram::BindAttribLocation(GLuint location, const char *name)
{
    glBindAttribLocation(handle, location, name);
}

inline void GLSLProgram::BindFragDataLocation(GLuint location, const char *name)
{
    glBindFragDataLocation(handle, location, name);
}

inline void GLSLProgram::SetUniform(const char *name, float x, float y, float z)
{
    GLuint loc = GetUniformLocation(name);
    glUniform3f(loc, x, y, z);
}

inline void GLSLProgram::SetUniform(const char *name, float x, float y, float z, float w)
{
    GLuint loc = GetUniformLocation(name);
    glUniform4f(loc, x, y, z, w);
}

inline void GLSLProgram::SetUniform(const char *name, const glm::vec2 &vec)
{
    GLuint loc = GetUniformLocation(name);
    glUniform2fv(loc, 1, glm::value_ptr(vec));
}

inline void GLSLProgram::SetUniform(const char *name, const glm::vec3 &vec)
{
    GLuint loc = GetUniformLocation(name);
    glUniform3fv(loc, 1, glm::value_ptr(vec));
}

inline void GLSLProgram::SetUniform(const char *name, const glm::vec4 &vec)
{
    GLuint loc = GetUniformLocation(name);
    glUniform4fv(loc, 1, glm::value_ptr(vec));
}

inline void GLSLProgram::SetUniform(const char *name, const glm::mat3 &mat)
{
    GLuint loc = GetUniformLocation(name);
    glUniformMatrix3fv(loc, 1, GL_FALSE, glm::value_ptr(mat));
}

inline void GLSLProgram::SetUniform(const char *name, const glm::mat4 &mat)
{
    GLuint loc = GetUniformLocation(name);
    glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(mat));
}

inline void GLSLProgram::SetUniform(const char *name, int val)
{
    GLuint loc = GetUniformLocation(name);
    glUniform1i(loc, val);
}

inline void GLSLProgram::SetUniform(const char *name, float val)
{
    GLuint loc = GetUniformLocation(name);
    glUniform1f(loc, val);
}

inline void GLSLProgram::SetUniform(const char *name, bool val)
{
    GLuint loc = GetUniformLocation(name);
    glUniform1i(loc, val);
}

#endif // GLSLPROGRAM_H
